package com.ant.backstage.config.shiro;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.ant.backstage.config.filter.LoadAuthentication;
import com.ant.backstage.config.jwt.JwtToken;
import com.ant.backstage.facade.ApiReturn;
import com.ant.backstage.facade.code.ApiReturnCode;
import com.ant.backstage.facade.dto.UserDTO;
import com.ant.backstage.facade.model.Authentication;
import com.ant.backstage.facade.model.Role;
import com.ant.backstage.facade.validation.UserConstant;
import com.ant.backstage.service.RegisterAndLoginService;
import com.ant.backstage.service.RoleService;
import com.ant.backstage.utils.DateTimeUtil;
import com.ant.backstage.utils.RedisUtil;
import com.ant.backstage.utils.ioc.ApplicationContextUtil;
import com.ant.backstage.utils.jwt.JwtUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * 请求拦截器
 *
 * @create 2020-08-22 12:06
 */
public class JwtAuthenticatingFilter extends BasicHttpAuthenticationFilter {

    private static Logger logger = LogManager.getLogger(JwtAuthenticatingFilter.class);
    // 是否刷新token
    public static boolean shouldRefreshToken = false;

    private static String LOGICAL_OR = "OR";
    private static String LOGICAL_AND = "AND";

    /**
     * 请求是否允许放行
     * 父类会在请求进入拦截器后调用该方法，返回true则继续，返回false则会调用onAccessDenied()。这里在不通过时，还调用了isPermissive()方法
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        boolean allowed = false;
        try {
            // 执行登录 true
            allowed = executeLogin(request, response);
            if (allowed) {
                //权限&角色校验
                allowed = userAuthorize(request, response);
                if (!allowed) {
                    logger.error("请求接口权限不足");
                    HttpServletResponse httpResponse = WebUtils.toHttp(response);
                    httpResponse.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value());
                }
            }
        } catch (IllegalStateException e) { //not found any token
            logger.error("Not found any token");
        } catch (Exception e) {
            logger.error("Error occurs when login" + e);
        }
        return allowed;
    }

    /**
     * 父类executeLogin()首先会createToken()，然后调用shiro的Subject.login()方法。
     * <p>
     * executeLogin()的逻辑是不是跟UserController里面的密码登录逻辑很像？
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 从请求头中的Authorization字段尝试获取jwt token
        String token = httpRequest.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            // 从请求头中的token字段（自定义字段）尝试获取jwt token
            token = httpRequest.getHeader("token");
        }
        if (StringUtils.isEmpty(token)) {
            // 从url参数中尝试获取jwt token
            token = httpRequest.getParameter("token");
        }
        if (!StringUtils.isEmpty(token)) {
        }
        String uid = JwtUtil.getClaimByKey(token, "uid");
        return new JwtToken(token, uid + UserConstant.USER_TOKEN_SUFFIX);
    }

    /**
     * 如果这个Filter在之前isAccessAllowed（）方法中返回false,则会进入这个方法。我们这里直接返回错误的response
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        ApiReturn apiReturn = new ApiReturn();
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=UTF-8");
        int status = httpResponse.getStatus();
        if (status == 405) {
            apiReturn.setApiReturnCode(ApiReturnCode.NO_PERMISSION);
        } else {
            apiReturn.setApiReturnCode(ApiReturnCode.LONGING_LOSE);
        }
        httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
        PrintWriter writer = response.getWriter();
        writer.println(JSONUtil.parse(apiReturn));
        writer.flush();
        fillCorsHeader(request, httpResponse);
        return false;
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) {

        JwtToken jwtToken = (JwtToken) token;
        RedisUtil redisUtil = ApplicationContextUtil.getBean(RedisUtil.class);
        //由于shiro认证和授权信息存入缓存时不能设置过期时间，所有这里认证信息的设置过期时间 当 key 不存在时，返回 -2 。 当 key 存在但没有设置剩余生存时间时，返回 -1
        String uid = JwtUtil.getClaimByKey(jwtToken.getToken(), "uid");
        long expire = redisUtil.getExpire(uid + UserConstant.USER_TOKEN_SUFFIX);
        if (expire <= -1) {
            redisUtil.expire(jwtToken.getToken(), 60 * 60);
        }
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        //token 刷新方案
        Date expireAt = JwtUtil.getExpireAt(jwtToken.getToken()); //请求token的有效时间
        int countDowSeconds = (int) DateTimeUtil.differSecondsBetween(
                LocalDateTime.now(), DateTimeUtil.toLocalDateTime(expireAt));
        RegisterAndLoginService registerAndLoginService = ApplicationContextUtil.getBean(RegisterAndLoginService.class);
        if (shouldRefreshToken && !StringUtils.isEmpty(expireAt) && countDowSeconds < 30) {
            Map<String, String> claims = new HashMap<>();
            claims.put("uid", uid);
            claims.put("refreshToken", "N");
            claims.put("time", String.valueOf(System.currentTimeMillis()));
            String newToken = registerAndLoginService.generateJwt(claims);

            RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
            SimpleAuthenticationInfo info = (SimpleAuthenticationInfo) redisTemplate.opsForValue().get(uid + UserConstant.USER_TOKEN_SUFFIX);
            redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());

            UserDTO userPo = (UserDTO) info.getPrincipals().getPrimaryPrincipal();
            userPo.setToken(newToken);
            redisUtil.del(uid + UserConstant.USER_TOKEN_SUFFIX);
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
            redisTemplate.opsForValue().set(uid + UserConstant.USER_TOKEN_SUFFIX, info, 60 * 60);
            redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());

            httpResponse.addHeader("Authorization", newToken);
            return true;
        }
        httpResponse.addHeader("Authorization", jwtToken.getToken());
        return true;
    }

    /**
     * 添加跨域支持
     *
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) {
        fillCorsHeader(request, response);
    }

    /**
     * 设置跨域
     */
    public void fillCorsHeader(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setStatus(HttpStatus.OK.value());
        }
    }

    /**
     * @return
     * @Author zhangyu
     * @Description
     * @Date 16:55 2022/1/15
     * @Param 1. 加载配置文件，获取接口的请求角色和权限
     **/
    public boolean userAuthorize(ServletRequest servletRequest, ServletResponse servletResponse) {
        ApiReturn notAuthorized = new ApiReturn();
        AtomicBoolean authorizeAllowed = new AtomicBoolean(false);
        // 请求前置 过滤
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        resp.setCharacterEncoding("utf-8");
        String uri = req.getRequestURI();
        Authentication authInfo = LoadAuthentication.getAuthInfo(uri); //接口权限信息对象

        // 配置文件中，不存在接口请求路径 未配置
        if (!Optional.ofNullable(authInfo).isPresent()) {
            notAuthorized.setApiReturnCode(ApiReturnCode.NO_PERMISSION);
            return authorizeAllowed.get();
        }
        List<String> types = authInfo.getType();
        //当前列表若包含某元素，返回结果为true, 若不包含该元素，返回结果为false。
        boolean typeflag = types.contains(Authentication.AUTH_TYPE_ANON);
        if (typeflag) {
            authorizeAllowed.set(true);
            return authorizeAllowed.get();
        }
        //1. 获取当前用户的角色和权限信息 缓存
        String token = WebUtils.toHttp(req).getHeader(UserConstant.AUTHORIZATION);
        RedisUtil redisUtil = ApplicationContextUtil.getBean(RedisUtil.class);
        String uid = JwtUtil.getClaimByKey(token, "uid");
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        SimpleAuthenticationInfo info = (SimpleAuthenticationInfo) redisTemplate.opsForValue().get(uid + UserConstant.USER_TOKEN_SUFFIX);
        redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());

        UserDTO userPo = (UserDTO) info.getPrincipals().getPrimaryPrincipal();
        if (!Optional.ofNullable(userPo).isPresent()) {
            return authorizeAllowed.get();
        }
        boolean b1 = redisUtil.hasKey(userPo.getUid() + UserConstant.USER_PERISSION);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        if (!b1) {
            //redis中不存在权限缓存 库中查
            RegisterAndLoginService registerAndLoginService = ApplicationContextUtil.getBean(RegisterAndLoginService.class);
            RoleService roleService = ApplicationContextUtil.getBean(RoleService.class);
            Set<String> permissions = registerAndLoginService.findPermissionsByUid(userPo.getUid()).stream().collect(Collectors.toSet());
            authorizationInfo.setStringPermissions(permissions);
            List<Role> roles = roleService.selectRolesByUid(userPo.getUid());
            Set<String> collect = roles.stream().distinct().map(Role::getRolename).collect(Collectors.toSet());
            authorizationInfo.setRoles(collect);
            redisUtil.set(userPo.getUid() + UserConstant.USER_PERISSION, authorizationInfo, 60 * 60);
        } else {
            authorizationInfo = (SimpleAuthorizationInfo) redisUtil.get(userPo.getUid() + UserConstant.USER_PERISSION);
        }
        Set<String> stringPermissions = authorizationInfo.getStringPermissions();//权限
        Set<String> roles = authorizationInfo.getRoles();//角色

        types.forEach(t -> {
            if (t.equals(Authentication.AUTH_TYPE_AUTHC)) {
                authorizeAllowed.set(checkPermission(stringPermissions, authInfo, authorizeAllowed.get()));
                authorizeAllowed.set(checkRole(stringPermissions, authInfo, authorizeAllowed.get()));
            }
            if (t.equals(Authentication.AUTH_TYPE_PREMS)) {
                authorizeAllowed.set(checkPermission(stringPermissions, authInfo, authorizeAllowed.get()));
            }
            if (t.equals(Authentication.AUTH_TYPE_ROLES)) {
                authorizeAllowed.set(checkRole(roles, authInfo, authorizeAllowed.get()));
            }
        });

        return authorizeAllowed.get();
    }

    public boolean checkPermission(Set<String> stringPermissions, Authentication authInfo, boolean finalAuthorizeAllowed) {
        if (Optional.ofNullable(stringPermissions).isPresent()) {
            List<String> permission = authInfo.getPermission();
            String permissionLogical = authInfo.getPermissionLogical();
            if (Optional.ofNullable(permission).isPresent()) {
                Set<String> collect = permission.stream().collect(Collectors.toSet());
                if (permissionLogical.equals(LOGICAL_OR)) {
                    stringPermissions.retainAll(collect);
                    if (stringPermissions.size() > 0) {
                        //说明有交集 满足其一
                        finalAuthorizeAllowed = true;
                    }
                } else {
                    //拥有全部角色
                    stringPermissions.removeAll(collect);//差集
                    if (stringPermissions.size() == 0) {
                        //说明有交集 满足其一
                        finalAuthorizeAllowed = true;
                    }
                }
            }
        } else {
            logger.error("接口权限配置异常,authInfo={},userPermissions={}",
                    JSONObject.toJSONString(authInfo), JSONObject.toJSONString(stringPermissions));
            finalAuthorizeAllowed = false;
        }
        return finalAuthorizeAllowed;
    }

    public Boolean checkRole(Set<String> roles, Authentication authInfo, boolean finalAuthorizeAllowed) {
        if (Optional.ofNullable(roles).isPresent()) {
            List<String> role = authInfo.getRole();
            String roleLogical = authInfo.getRoleLogical();
            if (Optional.ofNullable(role).isPresent()) {
                Set<String> collect = role.stream().collect(Collectors.toSet());
                if (roleLogical.equals(LOGICAL_OR)) {
                    roles.retainAll(collect);
                    if (roles.size() > 0) {
                        //说明有交集 满足其一
                        finalAuthorizeAllowed = true;
                    }
                } else {
                    //拥有全部角色
                    roles.removeAll(collect);//差集
                    if (roles.size() == 0) {
                        //说明有交集 满足其一
                        finalAuthorizeAllowed = true;
                    }
                }
            } else {
                logger.error("接口角色配置异常,authInfo={},roles={}",
                        JSONObject.toJSONString(authInfo), JSONObject.toJSONString(roles));
                finalAuthorizeAllowed = false;
            }
        }
        return finalAuthorizeAllowed;
    }


    /**
     这里我们使用了token进行登录认证，由于每次客户端请求服务器的时候都需要带上token，所以存在一定的风险被窃取，
     这样黑客极有可能拿着token来窃取服务器数据或者攻击，所以为了安全，token的过期时间往往设置的比较短，
     但是这样也带来一个问题，就是用户每过一段时间就需要重新登录认证，显然这样是不够友好的。

     解决办法：服务器在生成token的时候也会生成一个refresh-token，并把refresh-token保存在redis中，
     并把这两个token都发送给客户端，客户端将token和refresh-token保存的浏览器内存里，在请求数据的时候只带上token，
     服务器检测token有效后还比对 Token 中的时间戳与缓存中的 RefreshToken 时间戳是否一致，一致后才能请求到数据。
     当发现token过期的时候，客户端会使用refresh-token来向服务器请求刷新token，服务器会生成新的token和refresh-token，
     刷新redis中的refresh-token，并把两个新的token发送给客户端，这样一来，refresh-token只在第一次和刷新token的时候才会进行传输，
     就降低了被窃取的风险，黑客即便是拿到了token，但如果没有拿到refresh-token，短时间内的数据丢失和破坏也是在有限范围。
     同时通过设置refresh-token，还会带来另一个好处，就是我们可以通过控制服务器redis中的refresh-token来间接控制jwt的token认证。、
     **/

}